useRef 儲存的值不會因為元件重新渲染而改變,適合用來保存不需要觸發重新渲染的資料,像是表單輸入框的狀態、倒數計時器等。useRef 最常見的用途是用來存取 DOM 元素的引用,方便操作該元素。useRef 也常被用來將 DOM 元素的引用傳遞給第三方套件。除了一般常見的 useRef 定義方式,React 還提供了另一種方式叫 callback ref。callback ref 是將一個函數傳遞給 ref 屬性,在一些特殊情況很實用。
如果想自動讓某個元素 focus,一般常見的寫法如下:
import { useEffect, useRef } from "react";
export default function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement | null>(null);
  // 因為元素首次 render 還沒有 mounted,所以參數的預設值為 null。
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  return <input ref={inputRef} type="text" />;
}
但如果根據條件渲染元素時,ref 會是 null,所以即使執行 useEffect 還是不會 focus 。
import { useState, useEffect, useRef } from "react";
export default function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement | null>(null);
  const [showInput, setShowInput] = useState(false);
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  return (
    <div>
      <button onClick={() => setShowInput(true)}>Show Input</button>
      {showInput && <input ref={inputRef} type="text" />}
    </div>
  );
}
可以透過 callback ref 解決
import { useState, useCallback } from "react";
export default function AutoFocusInput() {
  const [showInput, setShowInput] = useState(false);
  const inputRef = useCallback((node: HTMLInputElement | null) => {
    node?.focus();
  }, []);
  return (
    <div>
      <button onClick={() => setShowInput(true)}>Show Input</button>
      {showInput && <input ref={inputRef} type="text" />}
    </div>
  );
}
透過 useCallback 避免每次渲染都重新建立 callback 函數。
可以參考 React 文件的這個例子。
其他更多使用情境可以參考這篇文章。
在 React 19 有新增了 callback cleanup function,類似 useEffect,當元件 unmount 時執行。
<input
  ref={(ref) => {
    // ref created
    // NEW: return a cleanup function to reset
    // the ref when element is removed from DOM.
    return () => {
      // ref cleanup
    };
  }}
/>
若要使用 React 19 ,要先更新到最新版本。
npm install --save-exact react@rc react-dom@rc
forwardRef 是一個高階函數 (high-order function),允許將 ref 轉發到函數元件內部的某個子元素。這樣就能夠在函數元件中獲取到內部 DOM 元素的引用。
functional components 預設是不支援直接使用 ref 的。這是因為 functional components 本身並沒有實例(instance),不像 class components 有 this 來指向元件實例。因此如果你直接在 functional components 上使用 ref,會無法取得預期的結果並出現錯誤訊息。
import { ComponentProps, forwardRef } from "react";
type ButtonProps = ComponentProps<"button"> & {
  icon?: React.ReactNode;
  text: string;
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ icon, text, ...props }, ref) => {
    return (
      <button
        ref={ref}
        className="flex items-center space-x-2 px-4 py-2 bg-blue-500 text-white rounded-md"
        {...props}
      >
        {icon && <span>{icon}</span>}
        <span>{text}</span>
      </button>
    );
  }
);
export default Button;
在 React 19 後就可以不用用到 forwardRef 了,可以直接使用 ref 作為 props。
function CustomInput({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}
// ...
<CustomInput ref={ref} />;
參考資料:
https://tkdodo.eu/blog/avoiding-use-effect-with-callback-refs
https://react.dev/learn/manipulating-the-dom-with-refs#
https://julesblom.com/writing/ref-callback-use-cases
https://react.dev/blog/2024/04/25/react-19
https://react.dev/reference/react/forwardRef#
https://www.youtube.com/watch?v=m4QbeS9BTNU